﻿using System;
using System.Collections;
using System.Collections.Generic;
using EnvDTE;
using EnvDTE80;

namespace VsTortoiseSVN.Commands
{
    public class ActiveDocumentFindPreviousModification : ActiveDocumentFindModification
    {
        public ActiveDocumentFindPreviousModification(DTE2 application, string name, string caption, string tooltip, int bitmapId)
            : base(application, name, caption, tooltip, bitmapId, ActiveDocumentFindModification.FindKind.Previous)
        {
            PreferredBinding = "Text Editor::Ctrl+Shift+Alt+Up Arrow";
        }
    }

    public class ActiveDocumentFindNextModification : ActiveDocumentFindModification
    {
        public ActiveDocumentFindNextModification(DTE2 application, string name, string caption, string tooltip, int bitmapId)
            : base(application, name, caption, tooltip, bitmapId, ActiveDocumentFindModification.FindKind.Next)
        {
            PreferredBinding = "Text Editor::Ctrl+Shift+Alt+Down Arrow";
        }
    }

    public class ActiveDocumentFindModification : ActiveDocumentBase
    {
        public enum FindKind
        {
            Next,
            Previous
        }

        private FindKind _findKind;
        private UnifiedDiffParser _diffParser;

        public ActiveDocumentFindModification(DTE2 application, string name, string caption, string tooltip, int bitmapId, FindKind kind)
            : base(application, name, caption, tooltip, bitmapId)
        {
            _findKind = kind;
            _diffParser = new UnifiedDiffParser();
        }

        public override vsCommandStatus QueryStatus()
        {
            if (Application.HasDocument())
            {
                SvnHelperStatusBools statuses = SvnHelperStatusBools.CreateVersioned();
                statuses.Ignored = false;
                statuses.Normal = false;

                if (SvnHelper.HasLocalTextStatus(FullName, statuses))
                    return vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled;
            }

            return vsCommandStatus.vsCommandStatusSupported;
        }

        public override bool Exec()
        {
            SharpSvn.SvnStatusEventArgs args;
            if (SvnHelper.FileStatus(FullName, out args))
            {
                Output.Instance.WriteLine("Getting unified diff...");

                byte[] unifiedDiff;
                if (SvnHelper.UnifiedLocalDiff(args.FullPath, out unifiedDiff))
                {
                    _diffParser.Parse(unifiedDiff);
                }
            }

            return FindModification(_findKind);
        }

        private bool FindModification(FindKind kind)
        {
            int line = _diffParser.FindModification(ActiveLine, _findKind == FindKind.Next ? 1 : -1, true);

            if (line != -1)
            {
                TextSelection selection = Application.ActiveDocument.Selection as TextSelection;
                selection.GotoLineCentered(line);
                return true;
            }

            return false;
        }
    }



    internal class UnifiedDiffParser
    {
        List<int> Added = new List<int>();

        public int FindModification(int line, int direction, bool wrap)
        {
            if (direction < 0)
                return FindPreviousModification(line, wrap);
            else
                return FindNextModification(line, wrap);
        }

        int FindPreviousModification(int line, bool wrap)
        {
            for (int n = Added.Count - 1; n >= 0; --n)
            {
                if (Added[n] < line)
                    return Added[n];
            }

            if (wrap)
                return FindPreviousModification(int.MaxValue, false);

            return -1;
        }

        int FindNextModification(int line, bool wrap)
        {
            // Try to find a modification beginning
            // at the specified line number.
            for (int n = 0; n < Added.Count; ++n)
            {
                if (Added[n] > line)
                    return Added[n];
            }

            // We are here when no modification has been
            // detected. Begin another search from the top.
            if (wrap)
                return FindNextModification(0, false);

            return -1;
        }

        public void Parse(byte[] text)
        {
            Added = ParseImpl(text);
        }

        List<int> ParseImpl(byte[] text)
        {
            var result = new List<int>();
            var modifiedline = 0;
            var guessed16bit = false; // whether the text seems to contain 16 bit characters

            var index = 0;
            while (index < text.Length)
            {
                var startindex = index++; // start index of line

                // read one line
                while (index < text.Length)
                {
                    if (text[index] == 0)
                        guessed16bit = true;

                    // handle CR+LF as one line only
                    if (index + 1 < text.Length && IsCR(text[index]) && IsLF(text[index + 1]))
                    {
                        index += 2;
                        break;
                    }

                    // handle 0+CR and 0+LF as one line only
                    if (index + 1 < text.Length && text[index] == 0 && (IsCR(text[index + 1]) || IsLF(text[index + 1])))
                    {
                        index += 2;
                        break;
                    }

                    if (IsCR(text[index]) || IsLF(text[index]))
                        break;

                    index++;
                }

                switch (text[startindex])
                {
                    case (byte)' ': // unchanged
                        modifiedline++;
                        break;

                    case (byte)'@': // hunk range (seems to be in ascii always)
                        if (startindex + 1 < text.Length && text[startindex + 1] == '@')
                        {
                            // hunk range example: @@ -40,7 +40,7 @@

                            // find the index after the plus symbol
                            var n = startindex + 2;
                            while (n < text.Length && text[n] != '+')
                                ++n;
                            var start = n;

                            // find the index of the comma
                            while (n + 1 < text.Length && text[n + 1] != ',')
                                ++n;
                            var end = n;

                            // calculate the line number, which is between the plus and comma symbols
                            modifiedline = 0;
                            for (n = end; n > start; --n)
                                modifiedline += (text[n] - 48) * (int)Math.Pow(10, end - n);
                        }
                        break;

                    case (byte)'-': // removed
                        // just ignore removed lines, do not change the modifiedline counter here
                        break;

                    case (byte)'+': // added
                        if (startindex + 1 < text.Length && text[startindex + 1] != '+')
                        {
                            result.Add(modifiedline);
                            modifiedline++;
                        }
                        break;
                }
            }

            // remove subsequent changes so we only have the start of the block of changes.
            // eg the list might contain line numbers 1,2,3,8 but we are interested in 1,8 only,
            // because those are the blocks with changes
            for (var n = result.Count - 1; n >= 1; --n)
            {
                if (result[n - 1] == result[n] - 1)
                    result.RemoveAt(n);
            }

            if (guessed16bit)
            {
                // if it seems to be 16 bit, we most likely have to double the
                // amount of lines due to 0|CR|0|LF rather than CR|LF
                // so half the line numbers
                for (var n = 0; n < result.Count; ++n)
                {
                    result[n] >>= 1;
                    result[n]++;
                }
            }

            if (Output.Instance.Enabled)
            {
                for (var n = 0; n < result.Count; ++n)
                    Output.Instance.WriteLine(string.Format("Line {0}", result[n]));
            }

            return result;
        }

        bool IsCR(byte v)
        {
            return v == 13;
        }

        bool IsLF(byte v)
        {
            return v == 10;
        }
    }

}
